package game.square;

import game.dungeon.position.Direction;
import game.square.temperature.Temperature;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;

public interface Square {

	/**
	 * Return the temperature of this square
	 */
	public abstract Temperature getTemperature();
	
	/**
	 * Set the temperature of this square to the given temperature.
	 * 
	 * @param 	temperature
	 *          The temperature that represents the temperature of this square, 
	 *          before transition to a new equilibrium starts.
	 * @post  	..
	 *        | for each square in getRoom()
	 *        | 	if( square.getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) < 
	 *        |			temperature.getTemperatureValue(TemperatureValue(TemperatureScale.CELCIUS))
	 *        |				square.getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) <= 
	 *        |						(new square).getTemperature().getTemperatureValue(TemperatureScale.CELCUIS)
	 *        |				&&					
	 *        |				(new square).getTemperature().getTemperatureValue(TemperatureScale.CELCUIS)<=
	 *        |						temperature.getTemperatureValue(TemperatureScale.CELCIUS)       					
	 * @post	..
	 * 		  | for each square in getRoom()
	 *		  | 	if( square.getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) > 
	 *        |			temperature.getTemperatureValue(TemperatureValue(TemperatureScale.CELCIUS))
	 *        |				square.getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) >= 
	 *        |						(new square).getTemperature().getTemperatureValue(TemperatureScale.CELCUIS)
	 *        |				&&					
	 *        |				(new square).getTemperature().getTemperatureValue(TemperatureScale.CELCUIS)>=
	 *        |						temperature.getTemperatureValue(TemperatureScale.CELCIUS)
	 * @throws	IllegalStateException
	 * 			The given temperature is not effective
	 * 		  | temperature == null
	 */

	public abstract void setTemperature(Temperature temperature);

	/**
	 * Return the cold damage of this square.
	 *  
	 * @return 	The cold damage of this square: one point for each 10 degrees 
	 * 			Celcius below -5 degrees Celcius. Rounded to the closest integer
	 * 			to zero.
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) <= -15)
	 * 		  |		then result == (-15 - getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)
	 * 		  |						)/10 + 1
	 *@result	Otherwise the result is 0
	 *		  |  	else result == 0 		
	 */
	public abstract int getColdDamage();


	/**
	 * Return the heat damage of this square.
	 * 
	 * @return 	The heat damage of this square: one point for each time the heat
	 *         	damage scale fits into the current temperature in Celcius.
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)) 
	 * 		  |  		>= getMinTempWithHeatDamge())
	 *		  |		then result == (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)
	 *		  |						 - getMinTempWithHeatDamage())/getScaleHeatDamage() + 1;	
	 *		  |	else result == 0;
	 */
	public abstract int getHeatDamage();


	/**
	 * Check if the given value is a valid humidity.
	 * 
	 * @param 	humidity
	 * 			The humidity to check
	 * @return	False if the given humidity is not effective.
	 * 		  | if (humidity == null)
	 * 		  |		then result == false
	 * @result 	False if the value of the given humidity is greater than 100
	 * 		  | if (humidity.compareTo(BigDecimal.valueOf(100)) == 1)
	 * 		  | 	then result == false
	 * @result 	False if the value of the given humidity is negative
	 * 		  | if (humidity.compareTo(BigDecimal.ZERO) == -1)
	 * 		  | 	then result == false
	 * @result 	False if the scale of the given humidity is less than 
	 * 			the humidity scale of all squares, true otherwise.
	 * 		  | result == !(humidity.scale() < getHumidityScale())
	 */
	@Raw
	public abstract boolean canHaveAsHumidity(BigDecimal humidity);

	/**
	 * Return the humidity of this square.
	 */
	@Basic
	public abstract BigDecimal getHumidity();

	/**
	 * Return the minimum humidity that results in rust damage.
	 */
	@Basic
	@Immutable
	public abstract int getMinHumidityWithRustDamage();

	/**
	 * Calculate the rust damage of this square.
	 * 
	 * @return 	The rust damage of this square: one point for each time the rust
	 *         	damage scale fits into the current humidity of this square.
	 * 		  | if ( getHumidity().compareTo(getMinHumidityWithRustDamage()) == -1)
	 *		  |		then result == 0	
	 *		  |	else result == getHumidity().subtract(getMinHumidityWithRustDamage()).
	 *								divide(getScaleRustDamage()).intValue() + 1	
	 */	 
	public abstract int getRustDamage();

	/**
	 * Return the slipperiness of this square.
	 * 
	 * @return  True if the floor of a square is made of a slippery material
	 * 		  | if(isSlipperyMaterial())
	 * 		  |		then result == true
	 * @result  False if the humidity of this square is lower or equal to 10
	 * 		  | if(getHumidity().floatValue() <= 10)
	 * 		  | 	then result == false
	 * @result  True if the temperature in Celcius of this square is negative
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) < 0)
	 * 		  |		then result == true
	 * @result	Otherwise true if the humidity is 100
	 * 		  |	getHumidity().compareTo((BigDecimal.valueOf(100))) == 0
	 */
	public abstract boolean isSlippery();

	/**
	 * return if the floor of a square is made of a slippery material or not.
	 */
	@Basic
	@Immutable
	public abstract boolean isSlipperyMaterial();

	/**
	 * Return a measure for the inhabitability of this square.
	 * 
	 * @return  The measure of inhabitability is calculated thus: - sqrt(heat
	 *          damage^3) / sqrt( 101 - humidity) - sqrt(cold damage) 
	 *        | result == -(Math.pow(getHeatDamage(), 1.5)/ 
	 *        | 	Math.pow(101-getHumidity().doubleValue(),0.5)) 
	 *        | 	- Math.pow(getColdDamage(),0.5)
	 */
	public abstract double getInhabitability();

	/**
	 * Check if this square is terminated.
	 */
	@Basic
	public abstract boolean isTerminated();


	/**
     * Terminate this square.
     *
     * @post    This square is terminated.
     *        | new.isTerminated()
     * @effect	All borders belonging to this square are terminated if
     * 			they don't share the border with an effective square.
     * 		  | 	For each direction in Direction.values()
     * 		  | 		if (hasAsBorderInDirection(direction))
     * 		  |				removeBorderInDirection(border)
     */
	public abstract void terminate();

	/**
	 * Return a copy of the borders of this square.
	 */
	@Basic
	public abstract Map<Direction, Border> getBorders();

	/**
	 * Get the direction of the given border in regard to this square.
	 * 
	 * @param	border
	 * 			The border to get the direction from.
	 * @return	The direction of the given border, if the given border 
	 * 			doesn't border this square than the result is null.
	 * 		  | result ==
	 *        |   {direction in getBorders().values() : getBorders().get(direction) == border}
	 */
	public abstract Direction getDirectionOf(Border border);

	/**
	 * Check whether this square has the given border as one
	 * of its borders.
	 * 
	 * @param	border
	 * 			The border to check.
	 * @return	True if and only if the given border has an effective
	 * 			direction
	 * 		  | result == (getDirectionOf(border) != null)
	 */
	@Raw
	public abstract boolean hasAsBorder(@Raw Border border);

	/**
	 * Check if this square has an effective border in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction to check for a border.
	 * @result	True if and only if this square has an effective border in
	 * 			the given direction.
	 * 		  | result == (getBorders.get(direction) != null) 
	 */
	public abstract boolean hasBorderInDirection(Direction direction);

	/**
	 * Check whether this square can have the given border
	 * as one of its borders in the given direction.
	 * 
	 * @param	border
	 * 			The border to check.
	 * @param	direction
	 * 			The direction of the given border.
	 * @return	False if the given direction is not effective
	 * 		  | if (direction == null)	
	 * @result	False if this square can't have the border in any direction.
	 * 		  | if (!canHaveAsBorder(border))
	 * 		  |		then result == false
	 * @result	Otherwise, false if the direction is floor and the given border
	 * 			is a door instance
	 * 		  |	else if (direction == Direction.FLOOR && (border instanceof Door))
	 * 		  |		then result == false
	 * @result	Otherwise the result is true.
	 * 		  | else result == true
	 */
	@Raw
	public abstract boolean canHaveAsBorderInDirection(@Raw Border border,
			Direction direction);

	/**
	 * Check whether this square can have the given border
	 * as one of its borders.
	 * 
	 * @param   border
	 *          The border to check.
	 * @return  False if the given border is not effective.
	 * 		  |	if (border == null)
	 * 		  |		then result == false
	 * @result	Otherwise, false if the given border or this square are terminated.
	 * 		  | else if (!border.isTerminated() || isTerminated())
	 * 		  |		then result == false
	 * @result	Otherwise, false if the given border can't have this
	 * 			square is one of the squares it borders.
	 * 		  |	else if (!border.canHaveAsSquare(this))
	 * 		  |		then result == false
	 * @result	Otherwise, false if the new number of borders is
	 * 			greater than six.
	 * 		  | else if(getNbBorders() + 1 > 6
	 * 		  |		then result == false
	 * @result	Otherwise, the result is true
	 * 		  | else result == true
	 */
	@Raw
	public abstract boolean canHaveAsBorder(@Raw Border border);

	/**
	 * Check whether this square has proper borders.
	 * 
	 * @return	False, if this square has no borders and is not terminated
	 * 			(the concept of	'no physical border' counts as a border).
	 * 		  |	if (getBorders().isEmpty() && !isTerminated())
	 * 		  |		then result == false
	 * @result	False if this square has more than three doors.
	 * 		  | if (getNbOfDoors() > 3)
	 * 		  |		then result == false
	 * @result	Otherwise false if at least one of the borders of this square
	 * 			is not effective or terminated.
	 * 		  | For each border in Border
	 * 		  |		if (hasAsBorder(border) && (border == null || border.isTerminated()))
	 * 		  |			then result == false
	 * @result	Otherwise false if at least one of the borders of this square doesn't reference
	 * 			this square.
	 * 		  | For each border in Border
	 * 		  |		if (hasAsBorder(border) && (!border.isBorderOf(this))
	 *		  |			then  result == false
	 * @result	Otherwise, false if at least one of the borders of this square can't have that
	 * 			border in the given direction.
	 * 		  |	For each border in Border
	 * 		  |		if (hasAsBorder(border) && !canHaveAsBorderInDirection(border,getDirectionOf(border))
	 * 		  |			then result == false
	 * @result	Otherwise, false if at least one of the borders of this square with an effective neighbor 
	 * 			doesn't has the same border in opposite directions.
	 * 		  |	For each border in Border
	 * 		  |		if (hasAsBorder(border) && border.getNeighborSquareFrom(this) != null &&
	 * 		  |				border.getNeighborSquareFrom(this).getDirectionOf(border) != 
	 * 		  |					getDirectionOf(border).getOppositeDirection())
	 * 		  |			then result == false
	 * @result	Otherwise the result is true.
	 * 		  | else result == true
	 */
	@Raw
	public abstract boolean hasProperBorders();

	/**
	 * Return the number of borders in this square.
	 * 
	 * @return  The total number of borders collected in this square.
	 *        | result ==
	 *        |   card({borders:Border | hasAsBorder(border)})
	 */
	public abstract int getNbBorders();

	/**
	 * Calculate the number of doors this square has.
	 * 
	 * @return	The number of borders of this square of type Door
	 * 		  | result == card({doors:Door | hasAsBorder(door)})
	 * 
	 */
	public abstract int getNbOfDoors();

	/**
	 *  Return the border of this square in the given direction
	 */
	@Basic
	public abstract Border getBorderInDirection(Direction direction);

	/**
	 * Return the neighbor of this square in the given direction.
	 * @param 	direction
	 * 			The direction from where you want the neighbor.
	 * @return	If this square has a border in the given direction,
	 * 			the result is the square who shares the same border in 
	 * 			the given direction.
	 * 		  |	if (getBorders().get(direction) != null)
	 * 		  |		then result == getBorders().get(direction).getNeighborSquareFrom(this)
	 * @result	Otherwise, the result is the null reference.
	 * 		  | else result == null
	 */
	public abstract Square getNeighborInDirection(Direction direction);

	/**
	 * Add the given border in the given direction to the set of borders registered in
	 * this square.
	 * 
	 * @param   border
	 *          The border to be added.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @post    The given border is registered as the border of this square in the
	 * 			given direction.
	 *        | new.getBorderInDirection(direction) == border.
	 * @effect	The given border has this square as one of the squares it borders.
	 * 		  | border.addBorderedSquare(this)
	 * @throws	IllegalStateException
	 * 			This square can't place the given border in the given direction
	 * 		  | !isPlaceableBorderInDirection(border,direction)
	 */
	public abstract void addBorderInDirection(Border border, Direction direction)
			throws IllegalStateException;

	/**
	 * Add the given border in the given direction to the set of borders registered in
	 * this square and to the set of borders in the given square (in the opposite direction
	 * of the given direction).
	 * 
	 * @param   border
	 *          The border to be added.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @param	neighbor
	 * 			The neighbor square to add the given border, in the opposite
	 * 			of the given direction, to.
	 * @post    The given border is registered as the border of this square in the
	 * 			given direction.
	 *        | new.getBorderInDirection(direction) == border.
	 * @post	The given border is registered as the border of the given neighbor
	 * 			in the opposite direction of the given direction.
	 * 		  | (new neighbor).getBorderInDirection(direction) == border
	 * @effect	The given border has this square as one of the squares it borders.
	 * 		  | border.addBorderedSquare(this)
	 * @effect	The given border has the given neighbor as one of the squares it borders.
	 * 		  | border.addBorderedSquare(neighbor)
	 * @throws	IllegalStateException
	 * 			This square can't place the given border in the given direction
	 * 		  | !isPlaceableBorderInDirection(border,direction)
	 * @throws	IllegalStateException
	 * 			The given neighbor can't place the given border in the opposite
	 * 			direction of the given direction.
	 * 		  | !neighbor.isPlaceableBorderInDirection(border, direction.getOppositeDirection())
	 */
	public abstract void addBorderInDirection(Border border,
			Direction direction, SquareImpl neighbor)
			throws IllegalStateException;

	/**
	 * Check if the given border is placeable at this square in the given 
	 * direction (at this moment).
	 * 
	 * @param   border
	 *          The border to check.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @return	False, if this square can't have the given border in the given direction
	 * 		  | if (!canHaveAsBorderInDirection(border,direction))
	 * 		  |		then result == false
	 * @result	False, if the given border is already placed.
	 * 		  |	if (border.isPlaced())
	 * 		  |		then result == false
	 * @result	False, if this square already has a border in the given direction.
	 * 		  |	if (hasBorderInDirection(direction))
	 * 		  | 	then result == false
	 * @result 	False, if this square already has the given border as one of its borders.
	 * 		  | if (hasAsBorder(border))
	 * 		  |		then result == false
	 * @result	False, if the given border is a door and this square already has two doors.
	 * 		  | if (getNbOfDoors() > 2 && border instance of Door)
	 * 		  |		then result == false
	 */
	public abstract boolean isPlaceableBorderInDirection(Border border,
			Direction direction);

	/**
	 * Add the given square as a neighbor of this square in the given direction
	 * by setting the same border for both squares (for this square in the given
	 * direction, for the given square in the opposite direction of the given
	 * direction) : the strongest border is picked (e.g. a door is stronger than
	 * no border, but weaker than a wall).
	 * 
	 * @param 	square
	 * 			The square to add
	 * @param 	direction
	 * 			The direction (from this square) of the border the given 
	 * 			square should be added to.
	 * @post	This square has the same border in the given direction as
	 * 			the given square in the opposite of the given direction.
	 * 		  |	new.getBorderInDirection(direction) == (new square).getBorderInDirection(
	 * 		  |			direction.getOppositeDirection())
	 * @post	The new border for both squares is stronger than, or equally
	 * 			strong as their old borders.
	 * 		  |	new.getBorderInDirection(direction).isStrongerThan(getBorderInDirection(direction) &&
	 * 		  | (new square).getBorderInDirection(direction).isStrongerThan(getBorderInDirection(direction))
	 * @throws	IllegalArgumentException
	 * 			The given direction is not effective
	 * 		  |	direction == null
	 * @throws	IllegalArgumentException
	 * 			Then given square is not effective
	 * 		  |	square == null
	 * @throws	IllegalArgumentException
	 * 			This square already has a neighbor in the given direction 
	 * 			or the given square already has a neighbor in the opposite
	 * 			direction of the given direction.
	 * 		  | getNeighborInDirection(direction) != null ||
	 *		  |		square.getNeighborInDirection(direction
	 * 		  |			.getOppositeDirection()) != null
	 * 			
	 */
	public abstract void addNeighborInDirection(SquareImpl square,
			Direction direction) throws IllegalArgumentException;

	/**
	 * Remove the border in the given direction from the borders registered in
	 * this square.
	 * 
	 * @param   direction
	 *          The direction of the border to remove.
	 * @pre     This square has a border in the given direction.
	 * 		  |	hasBorderInDirection(direction)
	 * @post    This square no longer has the given border as one
	 *          of its borders.
	 *        | ! new.hasAsBorder(this)
	 * @post	If this square has a neighbor in the given direction,
	 * 			the neighbor no longer has the shared border as one of
	 * 			its borders, but has a new border from the same class
	 * 			instead.
	 * 		  | if (getNeighborInDirection(direction) != null)
	 * 		  |		then new.getNeighborInDirection(direction).getBorderInDirection(direction.
	 * 		  |				getOppositeDirection()) != getNeighborInDirection(direction) &&
	 *		  |			 new.getNeighborInDirection(direction).getBorderInDirection(direction.
	 * 		  |				getOppositeDirection()).getClass() == getNeighborInDirection(direction).getClass()	 
	 */
	public abstract void removeBorderInDirection(Direction direction);

	/**
	 * Get the number of dungeons this square belongs to.
	 */
	@Basic
	public abstract int getNbOfDungeons();

	/**
	 * Increment the number of dungeons this square belongs to with one.
	 * 
	 * @post	The number of dungeons of this square is incremented with one.
	 * 		  | new.getNbOfDungeons() == getNbOfDungeons() + 1
	 */
	public abstract void incrementNbOfDungeons();

	
	/**
	 * Checks if the given square is in the same room as this square. The
	 * complexity is linear with the amount of squares in the room of this
	 * square.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @return	Return true if the given square borders this square and 
	 * 			the border between them is an open border
	 *		  | if (square.isDirectRoomNeighborFrom(this))
	 *		  |		result == true
	 * @result	Else return true if a the square this square borders 
	 * 			with an openable border, is in the same room as the given square.
	 * 		  | For someSquare in Square 
	 * 		  |		if (someSquare.isDirectRoomNeighborFrom(this))
	 * 		  |			if (someSquare.isInSameRoomAs(square))
	 * 		  |				result == true
	 * @result	Else, the result is false.
	 * 		  |	else result == false
	 */
	public abstract boolean isInSameRoomAs(Square square) ;

	/**
	 * Check if this square is a possible teleportation destination.
	 */
	@Basic
	@Immutable
	public abstract boolean isPossibleTeleportationDestination();

	/**
	 * Return the squares that belong to the same room as this square.
	 * 
	 * @return {squares in Square | square.isInSameRoomAs(this)}
	 */
	public abstract Set<? extends Square> getRoom();
	
	/**
	 * Check if the given square is a direct room neighbor from this square.
	 * 
	 * @param	square
	 * 			The square to check with.
	 * @return	True if  the given square and this square share
	 * 			the same openable border.
	 * 		  | For each border in getBorders().values()
	 * 		  |		if (border.getNeighborSquareFrom(this) == square &&
	 * 		  |		  border.isOpenBorder())
	 * 		  |			result == true
	 * @result	Otherwise, the result is false.
	 * 		  |	else result == false
	 */
	public abstract boolean isDirectRoomNeighborFrom(Square square);
	
	/**
	 * Checks if the given square is reachable from this square. 
	 * 
	 * @param 	square
	 * 			The square to check reachability from.
	 * 	
	 */
	public abstract boolean canReachSquare(Square square);

}